package com.avcompris.util.log;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static java.util.Locale.ENGLISH;
import static org.apache.commons.lang3.CharEncoding.UTF_8;
import static org.apache.commons.lang3.CharUtils.CR;
import static org.apache.commons.lang3.CharUtils.LF;
import static org.apache.commons.lang3.StringUtils.isBlank;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Formatter;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.util.AbstractUtils;

/**
 * utilities for logging in Java 5.
 * 
 * @author David Andriana Copyright Avantage Compris SARL 2009 ©
 */
public abstract class LogUtils extends AbstractUtils {

	/**
	 * log a formatted message if the level is DEBUG or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logDebugFormat(final Logger logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isDebugEnabled()) {

			logger.debug(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is DEBUG or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logDebugFormat(final Logger logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isDebugEnabled()) {

			logger.debug(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is DEBUG or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logDebugFormat(final Log logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isDebugEnabled()) {

			logger.debug(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is DEBUG or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logDebugFormat(final Log logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isDebugEnabled()) {

			logger.debug(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is INFO or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logInfoFormat(final Logger logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isInfoEnabled()) {

			logger.info(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is INFO or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logInfoFormat(final Logger logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isInfoEnabled()) {

			logger.info(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is INFO or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logInfoFormat(final Log logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isInfoEnabled()) {

			logger.info(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is INFO or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logInfoFormat(final Log logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isInfoEnabled()) {

			logger.info(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is WARN or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logWarnFormat(final Logger logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isEnabledFor(Level.WARN)) {

			logger.warn(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is WARN or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logWarnFormat(final Logger logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isEnabledFor(Level.WARN)) {

			logger.warn(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is WARN or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logWarnFormat(final Log logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isWarnEnabled()) {

			logger.warn(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is WARN or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logWarnFormat(final Log logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isWarnEnabled()) {

			logger.warn(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is ERROR or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logErrorFormat(final Logger logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isEnabledFor(Level.ERROR)) {

			logger.error(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is ERROR or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logErrorFormat(final Logger logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isEnabledFor(Level.ERROR)) {

			logger.error(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is ERROR or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logErrorFormat(final Log logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isErrorEnabled()) {

			logger.error(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is ERROR or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logErrorFormat(final Log logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isErrorEnabled()) {

			logger.error(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is FATAL or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logFatalFormat(final Logger logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isEnabledFor(Level.FATAL)) {

			logger.fatal(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is FATAL or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logFatalFormat(final Logger logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isEnabledFor(Level.FATAL)) {

			logger.fatal(formatted(format, args), exception);
		}
	}

	/**
	 * log a formatted message if the level is FATAL or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logFatalFormat(final Log logger,
			@Nullable final String format, @Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isFatalEnabled()) {

			logger.fatal(formatted(format, args));
		}
	}

	/**
	 * log a formatted message if the level is FATAL or more.
	 * 
	 * @param logger
	 *            the logger to use
	 * @param exception
	 *            the exception to log, if any
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format
	 */
	public static void logFatalFormat(final Log logger,
			@Nullable final Throwable exception, @Nullable final String format,
			@Nullable final Object... args) {

		nonNullArgument(logger, "logger");

		if (logger.isFatalEnabled()) {

			logger.fatal(formatted(format, args), exception);
		}
	}

	/**
	 * format a formatted message.
	 * 
	 * @param format
	 *            the message formatter
	 * @param args
	 *            the arguments to format within the message
	 * @return the formatted message
	 */
	private static String formatted(@Nullable final String format,
			@Nullable final Object... args) {

		if (format == null) {

			return null;
		}

		if (args == null || args.length == 0) {

			return format;
		}

		// synchronized (formatter) {

		return newFormatter().format(format, args).toString();

		// }
	}

	/**
	 * static {@link Formatter} instance for
	 * {@link #formatted(String, Object...)}.
	 */
	// private static final Formatter formatter =
	private static Formatter newFormatter() {

		return new Formatter(ENGLISH);
	}

	/**
	 * create a local log file, whether in the <tt>"target"</tt> directory, the
	 * local temp directory, or the current directory, in that order.
	 */
	public static LogFile dumpToFile(@Nullable final String fileName) {

		return dumpToFile(null, fileName);
	}

	public static LogFile dumpToFile(@Nullable final Throwable throwable,
			@Nullable final String fileName) {

		if (fileName == null) {

			return new DummyLogFile(fileName);
		}

		File dir = new File("target");

		if (!dir.exists() || !dir.isDirectory() || !dir.canWrite()) {

			dir = FileUtils.getTempDirectory();
		}

		if (!dir.exists() || !dir.isDirectory() || !dir.canWrite()) {

			dir = new File(".");
		}

		File file;

		final String suffix = "_" + System.currentTimeMillis();

		if (!dir.exists() || !dir.isDirectory() || !dir.canWrite()) {

			try {

				file = File.createTempFile(fileName, suffix);

			} catch (final IOException e) {

				file = null;
			}

		} else {

			file = new File(dir, fileName + suffix);
		}

		if (file == null
				|| (file.exists() && (!file.isFile() || !file.canWrite()))) {

			return new DummyLogFile(fileName);
		}

		System.err.println("Dumping log in: " + file.getAbsolutePath());

		final LogFile logFile = new DefaultLogFile(file);

		if (throwable != null) {

			logFile.write(throwable);
		}

		return logFile;
	}

	/**
	 * utility to dump data to a file. None of these methods throw any
	 * exception, all is silent.
	 */
	public interface LogFile {

		LogFile write(@Nullable byte[] bytes);

		LogFile write(@Nullable String text);

		LogFile write(@Nullable Throwable throwable);

		@Nullable
		File getFile();
	}

	private static class DefaultLogFile implements LogFile {

		private DefaultLogFile(final File logfile) {

			this.logfile = nonNullArgument(logfile, "logfile");

			String s;

			try {

				s = logfile.getCanonicalPath();

			} catch (final IOException e) {

				s = logfile.getAbsolutePath();
			}

			this.s = "[logfile:" + s + "]";
		}

		private final String s;

		private final File logfile;

		@Override
		public String toString() {

			return s;
		}

		@Override
		public LogFile write(@Nullable final byte[] bytes) {

			if (bytes == null) {

				return write("bytes: null");
			}

			write("bytes: " + bytes.length);

			try {

				FileUtils.writeByteArrayToFile(logfile, bytes, true);

				FileUtils.writeByteArrayToFile(logfile, CRLF, true);

			} catch (final IOException e) {
				e.printStackTrace();
			} catch (final RuntimeException e) {
				e.printStackTrace();
			}

			return this;
		}

		@Override
		public LogFile write(@Nullable final String text) {

			if (text == null) {

				return write("String: null");
			}

			if (isBlank(text)) {

				return write("String: blank");
			}

			try {

				FileUtils.writeStringToFile(logfile, text, UTF_8, true);

				FileUtils.writeByteArrayToFile(logfile, CRLF, true);

			} catch (final IOException e) {
				e.printStackTrace();
			} catch (final RuntimeException e) {
				e.printStackTrace();
			}

			return this;
		}

		@Override
		public LogFile write(@Nullable final Throwable throwable) {

			if (throwable == null) {

				return write("Throwable: null");
			}

			try {

				final OutputStream os = new FileOutputStream(logfile, true);
				try {

					throwable.printStackTrace(new PrintWriter(os, true));

				} finally {
					os.close();
				}

				FileUtils.writeByteArrayToFile(logfile, CRLF, true);

			} catch (final IOException e) {
				e.printStackTrace();
			} catch (final RuntimeException e) {
				e.printStackTrace();
			}

			return this;
		}

		@Override
		public File getFile() {

			return logfile;
		}

		private static byte[] CRLF = new byte[] { CR, LF };
	}

	private static class DummyLogFile implements LogFile {

		public DummyLogFile(@Nullable final String name) {

			System.err
					.println("Cannot create log file to dump: LogUtils.dumpToFile("
							+ name + ")");

			this.name = name;
		}

		private final String name;

		@Override
		public LogFile write(@Nullable final byte[] bytes) {

			return this;
		}

		@Override
		public LogFile write(@Nullable final String text) {

			return this;
		}

		@Override
		public LogFile write(@Nullable final Throwable throwable) {

			return this;
		}

		@Override
		public String toString() {

			return "[No log file could be created for: " + name + "]";
		}

		@Override
		@Nullable
		public File getFile() {

			return null;
		}
	}
}
